﻿#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<assert.h>

// 1.程序的翻译环境和执行环境
//
//在ANSI C（指的是标准c语言）的任何一种实现中，存在两个不同的环境。
//第1种是翻译环境，在这个环境中源代码被转换为可执行的机器指令（二进制指令）
//第2种是执行环境，它用于实际执行代码。
//
//test.c          --------->                      test.exe
//可以写一个代码，然后点开生成解决方案，就会生成一个.exe的文件
//从test.c到test.exe的过程中，依赖的就是我们的翻译环境（vs2022充当了翻译环境)
//
//
// 2.详解编译 + 链接
//
//翻译环境细分就可以分为编译和＋链接
//编译又分为三个过程：预编译（预处理）、编译、汇编
//
//2.1翻译环境
// 1）组成一个程序的每个源文件通过编译过程分别转换成目标代码（object code）
// 2）每个目标文件由链接器（linker）捆绑在一起，形成一个单一而完整的可执行程序
// 3）链接器同时也会引入标准C函数库中任何被该程序所用到的函数，而且它可以搜索程序员个人的程序库，
//将其需要的函数也链接到程序中
//
//2.2编译本身也分为几个阶段：
// 
//观察add.c和test.c
//
//test.c
int main()
{
	//extern是声明外部的变量
	extern void print(char* str);
	extern int g_val;
	printf("%d\n", g_val);
	print("hello bit.\n");
	int a = 2;
	int b = 3;
	//extern声明外部变量
	extern int add(int x, int y);
	printf("%d\n", add(a, b));

	return 0;
}

//1. 预处理 选项 gcc -E test.c -o test.i 预处理完成之后就停下来，预处理之后产生的结果都放在test.i文件中。
//2. 编译 选项 gcc - S test.c 编译完成之后就停下来，结果保存在test.s中。
//3. 汇编 gcc - c test.c 汇编完成之后就停下来，结果保存在test.o中，这个test.o就是目标文件
//
//编译的三个阶段
//第一个阶段（文本操作）：预处理阶段，进行;1.#include的包含   2.#define的定义符号的替换和删除   3.注释的删除
//第二个阶段：编译阶段，把c语言的代码翻译成了汇编代码，进行语法分析、词法分析、语义分析、符号汇总
//第三个阶段：汇编阶段，把汇编指令翻译成二进制指令（存放在目标文件中），形成符号表
//
//链接阶段：
//1.合并段表
//2.符号表的合并和重定位
//
//注意：在符号表中，只能看到像函数、全局变量等，看不到局部类变量符号
//
//2.3运行环境
//程序的执行过程
//1. 程序必须载入内存中。在有操作系统的环境中：一般这个由操作系统完成。在独立的环境中，程序的载入必须
//由手工安排，也可能是通过可执行代码置入只读内存来完成。
//2. 程序的执行便开始。接着便调用main函数。
//3. 开始执行程序代码。这个时候程序将使用一个运行时堆栈（stack），存储函数的局部变量和返回地址。程序同
//时也可以使用静态（static）内存，存储于静态内存中的变量在程序的整个执行过程一直保留他们的值。
//4. 终止程序。正常终止main函数；也有可能是意外终止。
//
//
//3.预处理详解
//
//3.1预定义符号
//
//__FILE__        进行编译的源文件
//__LINE__        文件当前的行号
//__DATE__        文件被编译的日期
//__TIME__        文件被编译的时间
//__STDC__        如果编译器遵循ANSI C，其值为1，否则未定义
//__FUNCTION__    当前编译的函数名
 
int main()
{
	printf("%s\n", __FILE__);
	printf("%d\n", __LINE__);
	printf("%s\n", __DATE__);
	printf("%s\n", __TIME__);
	printf("%d\n", __STDC__);//说明vs2022不遵循ANSI C这个编译标准
	printf("%s\n", __FUNCTION__);
	return 0;
}

//预处理指令：
//#define
//#include
//#pragma
//……


//3.2#define（预处理指令）

//3.2.1#define定义标识符

//语法：
#define name stuff
//就是定义一个标识符，名字叫name，内容是stuff

//举个例子：
#define MAX 1000
#define reg register          //为 register这个关键字，创建一个简短的名字
#define do_forever for(;;)     //用更形象的符号来替换一种实现
#define CASE break;case        //在写case语句的时候自动把 break写上。
// 如果定义的 stuff过长，可以分成几行写，除了最后一行外，每行的后面都加一个反斜杠(续行符)。
#define DEBUG_PRINT printf("file:%s\tline:%d\t \
                          date:%s\ttime:%s\n" ,\
                          __FILE__,__LINE__ ,       \
                          __DATE__,__TIME__ )  

//指令使用1：定义一个死循环
#define do_forever for(;;)
int main()
{
	//写个死循环
	do_forever;
	return 0;
}

//指令使用2：case语句
#define CASE break;case 
int main()
{
	int n = 0;
	scanf("%d", &n);
	switch (n)
	{
	case1:
	CASE 2:
	CASE 3:
	CASE 4:
	}
	return 0;
}

//提个问题：
//在deﬁne定义标识符的时候，要不要在最后加上 ; 
#define MAX 1000
#define MAX 1000;
//建议不要加上 ; ,这样容易导致问题。 比如下面的场景：
int main()
{
	int n = 0;
	int max = 0;
	if (n==0)
		max = MAX;
	else
		max = 0;
	return 0;
}

//3.2.2#define定义宏

//#deﬁne 机制包括了一个规定，允许把参数替换到文本中，这种实现通常称为宏（macro）或定义宏（deﬁnemacro）


//下面是宏的申明方式：
//#define name( parament-list ) stuff 其中的 parament-list 是一个由逗号隔开的符号表，它们可能出现在stuﬀ中
//parament-list就是宏的参数，name就是宏的名字
//注意： 
//1.参数列表的左括号必须与name紧邻
//2.如果两者之间有任何空白存在，参数列表就会被解释为stuﬀ的一部分

//如：
#define SQUARE( x ) x * x

//如何让使用呢：
#define SQUARE( x ) x * x
int main()
{
	printf("%d\n", SQUARE(5));
	return 0;
}

//注：宏和函数的区别：
//宏是直接替换过去的，而函数是会先计算在传参过去，是可以传表达式的，而宏不可以传表达式
//所以，如果将以上的宏定义运用以下代码，就会出现问题：
int main()
{
	printf("%d\n", SQUARE(5 + 1));//最后输出的结果是11
	return 0;
}
//正是因为宏在使用的时候，它是直接替换而不是传表达式的值过去的，所以，将以上的宏定义改为这样更好：
#define SQUARE(x) ((x)*(x))


//3.2.3#define的替换规则

//在程序中扩展#deﬁne定义符号和宏时，需要涉及几个步骤
//1. 在调用宏时，首先对参数进行检查，看看是否包含任何由#deﬁne定义的符号。如果是，它们首先被替换
//2. 替换文本随后被插入到程序中原来文本的位置。对于宏，参数名被他们的值替换
//3. 最后，再次对结果文件进行扫描，看看它是否包含任何由#deﬁne定义的符号。如果是，就重复上述处理过程

//注意：
//1. 宏参数和#deﬁne 定义中可以出现其他#deﬁne定义的变量。但是对于宏，不能出现递归。
//2. 当预处理器搜索#deﬁne定义的符号的时候，字符串常量的内容并不被搜索。

//例如：
#define M 100
int main()
{
	printf("hehe M\n");//此处的M不会被替换
	return 0;
}

//3.2.4 #和##

//1.#的作用：

//如何把参数插入字符串
//首先我们看这个代码
int main()
{
	char* p = "hello ""bit\n";
	printf("hello", " bit\n");
	printf("%s", p);
	return 0;
}
//这个代码说明：字符串是有自动连接的特点的

//那我们再来看一个代码：
int main()
{
	int a = 10;
	printf("the value of a is %d\n", a);

	int b = 1;
	printf("the value of b is %d\n", b);
	return 0;
}
//那我们能不能用一个宏将这个类似的语句给封装一下呢？
//根据上面字符串的特点，我们可以做一下处理

#define PRINT(x) printf("the value of"#x"is %d\n",x)
int main()
{
	int a = 0;
	PRINT(a);

	int b = 1;
	PRINT(b);
	return 0;
}
//上述宏定义的时候，我们会在x处多加一个#，这个#号的作用就是将它转换成我们对应的字符串
//这里就做到了如何把参数插入到字符串中
//另外一个技巧是： 使用 # ，把一个宏参数变成对应的字符串。
#define PRINT(FORMAT, VALUE) printf("the value of"#VALUE"is" FORMAT"\n",VALUE)
int main()
{
	float i = 3.14;
	PRINT("%f", i);
	return 0;
}

//2.##的作用：

//##可以把位于它两边的符号合成一个符号。 它允许宏定义从分离的文本片段创建标识符。

//例如：
#define consist(x,y) x##y
int main()
{
	int Class109 = 2023;
	printf("%d\n", consist(Class, 109));//输出结果是2023
	return 0;
}
//注： 这样的连接必须产生一个合法的标识符。否则其结果就是未定义的

//3.2.5带副作用的宏参数

//当宏参数在宏的定义中出现超过一次的时候，如果参数带有副作用，那么你在使用这个宏的时候就可能出现危险，导
//致不可预测的后果。副作用就是表达式求值的时候出现的永久性效果。 例如：
int main()
{
	int a = 10;
	int b = a + 1;//没有副作用
	int b = ++a;//有副作用,因为在给b赋值的时候，页同时影响力a
	return 0;
}

//下面来看一个比较两个数大小的宏
#define MAX(a, b) ( (a) > (b) ? (a) : (b) )
int main()
{
	int x = 5;
	int y = 8;
	int z = MAX(x++, y++);
	printf("x=%d y=%d z=%d\n", x, y, z);//输出的结果是什么？
	return 0;
}
//预处理器处理之后的结果是：
// z = ((x++) > (y++) ? (x++) : (y++));

//所以输出的结果是：x=6 y=10 z=9



//3.2.6宏和函数的对比

//宏通常被应用于执行简单的运算。比如在两个数中找出较大的一个
#define MAX(a, b) ((a)>(b)?(a):(b))

//那为什么不用函数来完成这个任务？ 原因有二：
//1. 用于调用函数和从函数返回的代码可能比实际执行这个小型计算工作所需要的时间更多。所以宏比函数在程序
//的规模和速度方面更胜一筹。
//2. 更为重要的是函数的参数必须声明为特定的类型。所以函数只能在类型合适的表达式上使用。反之这个宏怎可
//以适用于整形、长整型、浮点型等可以用于 > 来比较的类型
//综上所述：宏是类型无关的。

//当然和宏相比函数也有劣势的地方：
//1. 每次使用宏的时候，一份宏定义的代码将插入到程序中。除非宏比较短，否则可能大幅度增加程序的长度
//2. 宏是没法调试的，因为宏的代码在预处理阶段就被宏对应的内容替换了，所以，你要执行时候的代码和你调试时候的
//代码是不一样的，所以没法调试
//3. 宏由于类型无关，也就不够严谨
//4. 宏可能会带来运算符优先级的问题，导致程容易出现错，所以在进行宏的定义的时候，就尽量要多的使用括号

//宏有时候可以做函数做不到的事情。比如：宏的参数可以出现类型，但是函数做不到

#define MALLOC(num, type) (type*)malloc(num * sizeof(type))
int main()
{
	int* p = MALLOC(10, int);
	//预处理器替换之后： int* p = (int*)malloc(10 * sizeof(int));
	return 0;
}

//宏和函数的对比：
//如果代码的逻辑比较简单，就使用宏，如果代码逻辑比较复杂，那么就使用函数


//3.2.7命名的约定

//一般来讲函数的宏的使用语法很相似。所以语言本身没法帮我们区分二者。 那我们平时的一个习惯是：
//把宏名全部大写 函数名不要全部大写

//3.3#undef

//这条指令用于移除一个宏定义
#undef NAME 
//如果现存的一个名字需要被重新定义，那么它的旧名字首先要被移除。

//例如：
#define MAX 1000
int main()
{
	printf("%d\n", MAX);
#undef MAX
	printf("%d\n", MAX);//MAX宏定义已经被取消，所以下面就不能使用MAX了
	return 0;
}


//3.4命令行定义(命令行编译器）

//许多C 的编译器提供了一种能力，允许在命令行中定义符号。用于启动编译过程。 例如：当我们根据同一个源文件要
//编译出不同的一个程序的不同版本的时候，这个特性有点用处。（假定某个程序中声明了一个某个长度的数组，如果
//机器内存有限，我们需要一个很小的数组，但是另外一个机器内存大写，我们需要一个数组能够大写。）

//代码：
int main()
{
	int array[ARRAY_SIZE];
	int i = 0;
	for (i = 0; i < ARRAY_SIZE; i++)
	{
		array[i] = i;
	}
	for (i = 0; i < ARRAY_SIZE; i++)
	{
		printf("%d ", array[i]);
	}
	printf("\n");
	return 0;
}
//编译指令：
//gcc -D ARRAY_SIZE=10 -o programe.c

//3.5条件编译

//在编译一个程序的时候我们如果要将一条语句（一组语句）编译或者放弃是很方便的。因为我们有条件编译指令。
//比如说：
//调试性的代码，删除可惜，保留又碍事，所以我们可以选择性的编译。

#define __DEBUG__ 
int main()
{
	int i = 0;
	int arr[10] = { 0 };
	for (i = 0; i < 10; i++)
	{
		arr[i] = i;
#ifdef __DEBUG__ 
		printf("%d\n", arr[i]);//为了观察数组是否赋值成功。 
#endif //如果定义了__DEBUG__，那么就执行这个代码，否则不执行 ，这个定义可以给值也可以不给值
	}
	return 0;
}

//常见的条件编译指令：

//1. 
//#if 常量表达式 
   //... 
//#endif 
//常量表达式由预处理器求值。 

//例如：
#define __DEBUG__ 1 
int main()
{
#if __DEBUG__ //或者这里直接写成1
	printf("haha\n");
#endif 
	return 0;
}

//2.多个分支的条件编译
//#if 常量表达式 
  //... 
//#elif 常量表达式 
  //... 
//#else 
  //... 
//#endif 

//例如：
#define MAX 5
int main()
{
#if MAX==1
	printf("hehe\n");
#elif MAX==5
	printf("haha\n");
#else 
	printf("heihei\n");
#endif
	return 0;
}

//3.判断是否被定义(这里值判断是否定义，不判断真假）
//#if defined(symbol) 
//#ifdef symbol 
   //……
//#endif

//#if !defined(symbol) 
//#ifndef symbol 
   //……
//#endif

//例如：
#define MAX 0
int main()
{
#if defined(MAX)
	printf("haha\n");
#endif
	return 0;
}

//4.嵌套指令
// #if defined(OS_UNIX) 
//     #ifdef OPTION1 
		  unix_version_option1();
//     #endif 
//     #ifdef OPTION2 
		  unix_version_option2();
//     #endif 
// #elif defined(OS_MSDOS) 
//     #ifdef OPTION2 
		  msdos_version_option2();
//     #endif 
// #endif


//3.6头文件的包含

//我们已经知道， #include 指令可以使另外一个文件被编译。就像它实际出现于 #include 指令的地方一样。
//这种替换的方式很简单： 预处理器先删除这条指令，并用包含文件的内容替换。 这样一个源文件被包含10次，那就
//实际被编译10次。

//3.6.1头文件被包含的方式：

//（1）本地文件的包含
#include "filename"//filename就是本地文件名

//查找策略：先在源文件所在目录下查找，如果该头文件未找到，编译器就像查找库函数头文件一样在标准位置查找头
//文件。 如果找不到就提示编译错误。

//linux环境的标准头文件的路径：/usr/include
//VS环境的标准头文件的路径：C:\Program Files(x86)\Microsoft Visual Studio 9.0\VC\include
//注意按照真假的安装路径去找


//（2）库文件的包含
#include <filename.h>//filename就是本地文件名

//查找头文件直接去标准路径下去查找，如果找不到就提示编译错误。
//这样是不是可以说，对于库文件也可以使用 “” 的形式包含？ 答案是肯定的，可以。
//但是这样做查找的效率就低些，当然这样也不容易区分是库文件还是本地文件了。



//3.6.2嵌套文件包含

//在公司进行多人合作些项目的时候，看你会导致几个程序员的程序合并时同时都包含一样的头文件
//这样会浪费空间资源，那么如何解决这个问题呢？
//使用条件编译

//每个头文件里面都这样写
//（1）写法1
#ifndef __TEST_H__ 
#define __TEST_H__ 
//头文件的内容 
#endif //__TEST_H__
//例如：
#ifndef __TEST_H__ 
#define __TEST_H__ 
		  int Add(int x, int y);
#endif //__TEST_H__

//（2）写法2
#pragma once
//就可以避免头文件的重复引入。
		 

//两个练习题目：
//1. 头文件中的 ifndef/define/endif是干什么用的? 
//防止头文件被重复包含的

//2. #include <filename.h> 和 #include "filename.h"有什么区别 ?
//<>的查找方式比较直接，直接取库里面查找，而""首先去代码所在的工程下找，找不到再去库里面找
